package edu.nus.cs4243.recon.ui;

import java.io.IOException;
import java.util.Set;

import org.eclipse.swt.SWT;
import org.eclipse.swt.events.MouseEvent;
import org.eclipse.swt.events.MouseListener;
import org.eclipse.swt.events.MouseMoveListener;
import org.eclipse.swt.events.MouseWheelListener;
import org.eclipse.swt.events.PaintEvent;
import org.eclipse.swt.events.PaintListener;
import org.eclipse.swt.graphics.ImageData;
import org.eclipse.swt.graphics.Point;
import org.eclipse.swt.graphics.Rectangle;
import org.eclipse.swt.widgets.Canvas;
import org.eclipse.swt.widgets.Composite;
import org.eclipse.swt.widgets.Display;

import edu.nus.cs4243.recon.model.Feature;
import edu.nus.cs4243.recon.model.HasCoordinates2d;
import edu.nus.cs4243.recon.model.Image;
import edu.nus.cs4243.recon.model.Point2d;
import edu.nus.cs4243.recon.model.project.Project;

import static edu.nus.cs4243.recon.ui.ImageControlGlobalState.getState;

/**
 * Control capable of displaying an image (including zooming, panning) and adding/moving features.
 * 
 * @author johannes
 */
public class ImageControl extends Canvas implements PaintListener, MouseListener,
		MouseMoveListener, MouseWheelListener {

	/**
	 * The current type of dragging
	 * 
	 * @author johannes
	 */
	private static enum DragType {
		/**
		 * No dragging (i.e., the mouse is not pressed)
		 */
		NONE, 
		
		/**
		 * Moving the image
		 */
		PAN, 
		
		/**
		 * Moving a feature
		 */
		FEATURE,
		
		/**
		 * Creating (or removing) a connection between features
		 */
		CONNECTION
	}

	private final String CHOOSE_IMAGE = "Please choose an image from the list";
	private final String DROP_IMAGE = "or drop an image on this window.";
	private final double ZOOM_SPEED = 0.95;
	private final double MAX_ZOOM = 16.0;
	private final double MIN_ZOOM = 0.0625;
	private final double MAX_PIXEL_DIST = 6;

	private DragType dragType = DragType.NONE;
	private Point mousePos = new Point(0, 0);
	private Image visibleImage = null;
	private org.eclipse.swt.graphics.Image image = null;
	/**
	 * The picture coordinate that is in the center of the visible view.
	 */
	private double scrollX, scrollY;
	/**
	 * The zoom factor. (E.g. 0.5 = the image will be displayed half as big as it is)
	 */
	private double zoom = 1.0;

	/**
	 * Create a new image control.
	 * 
	 * @param parent
	 * @param style
	 */
	public ImageControl(Composite parent, int style) {
		super(parent, style);

		addPaintListener(this);
		addMouseListener(this);
		addMouseMoveListener(this);
		addMouseWheelListener(this);

		getState().addImageControl(this);
	}

	/**
	 * Set the image to be displayed by this ImageControl.
	 * 
	 * @param img
	 * @throws SWTException
	 *             if the image does not exist or is invalid
	 */
	public void setVisibleImage(Image img) {
		if (image != null) {
			image.dispose();
		}
		visibleImage = img;
		if (visibleImage != null) {
			image = new org.eclipse.swt.graphics.Image(Display.getDefault(), visibleImage.getPath()
					.getAbsolutePath());
		} else {
			image = null;
		}
	}

	/**
	 * Return the image that is currently being displayed.
	 * 
	 * @return
	 */
	public Image getVisibleImage() {
		return visibleImage;
	}

	@Override
	public void mouseDoubleClick(MouseEvent e) {
		if (zoom < 1.5)
			zoom = 3.0;
		else
			zoom = 1.0;
		redraw();
	}

	@Override
	public void mouseDown(MouseEvent e) {
		if (visibleImage == null)
			return;
		
		Point2d worldCoords = viewToWorld(new Point2d(e.x, e.y));
		if (e.button == 1) {
			if ((e.stateMask & SWT.SHIFT) > 0 && getState().getSelectedFeature() != null) {
				dragType = DragType.FEATURE;
				visibleImage.setLocation(getState().getSelectedFeature(), worldCoords);
				redraw();
			} else if ((e.stateMask & SWT.CONTROL) > 0) {
				Feature selectedFeature = new Feature();
				visibleImage.setLocation(selectedFeature, worldCoords);
				getState().setSelectedFeature(selectedFeature);
				dragType = DragType.FEATURE;
			} else {
				// is there a feature nearby?
				Feature minFeature = visibleImage.getNearestFeature(worldCoords);
				if (minFeature != null
						&& visibleImage.getLocationInView(minFeature).distanceTo(worldCoords) < MAX_PIXEL_DIST
								/ zoom) {
					getState().setSelectedFeature(minFeature);
					dragType = DragType.FEATURE;
				} else {
					dragType = DragType.PAN;
				}
			}
		} else if (e.button == 3) {
			// is there a feature nearby?
			// TODO remove duplicate code
			Feature minFeature = visibleImage.getNearestFeature(worldCoords);
			if (minFeature != null
					&& visibleImage.getLocationInView(minFeature).distanceTo(worldCoords) < MAX_PIXEL_DIST
							/ zoom) {
				getState().setSelectedFeature(minFeature);
				dragType = DragType.CONNECTION;
			} else {
				dragType = DragType.NONE;
			}
		}
	}

	@Override
	public void mouseUp(MouseEvent e) {
		if (dragType == DragType.FEATURE) {
			try {
				visibleImage.saveFeatures();
			} catch (IOException e1) {
				System.err.println("Could not save image features: " + visibleImage.getPath());
			}
		} else if (dragType == DragType.CONNECTION){
			System.out.println("Foo");
			Point2d worldCoords = viewToWorld(new Point2d(e.x, e.y));
			Feature minFeature = visibleImage.getNearestFeature(worldCoords);
			if (minFeature != null && visibleImage.getLocationInView(minFeature).distanceTo(worldCoords) < MAX_PIXEL_DIST
							/ zoom) {
				getState().getProject().toggleConnection(getState().getSelectedFeature(), minFeature);
			}
		}
		dragType = DragType.NONE;
	}

	@Override
	public void mouseMove(MouseEvent e) {
		switch (dragType) {
		case PAN:
			scrollX += (mousePos.x - e.x) / zoom;
			scrollY += (mousePos.y - e.y) / zoom;
			break;
		case FEATURE:
			Feature f = getState().getSelectedFeature();
			if (f == null)
				dragType = DragType.NONE;
			else {
				visibleImage.setLocation(f, viewToWorld(new Point2d(e.x, e.y)));
				getState().notifyFeatureChange(f);
			}
		}
		mousePos.x = e.x;
		mousePos.y = e.y;
		redraw();
	}

	/**
	 * Convert a Point from view coordinates to world (i.e. image) coordinates.
	 * 
	 * @param p
	 *            a point in client area coordinates (i.e. top left = 0/0)
	 * @return
	 */
	private Point2d viewToWorld(HasCoordinates2d p) {
		// get location relative to center of view
		Rectangle ca = getClientArea();
		double x = p.getX() - ca.x - ca.width / 2;
		double y = p.getY() - ca.y - ca.height / 2;
		// apply zoom factor
		x /= zoom;
		y /= zoom;
		// translate to image coordinates
		x += scrollX;
		y += scrollY;
		return new Point2d(x, y);
	}

	/**
	 * Convert a Point from image coordinates to view coordinates.
	 * 
	 * @param p
	 *            a point in image coordinates
	 * @return
	 */
	private Point2d worldToView(HasCoordinates2d p) {
		double x = p.getX();
		double y = p.getY();
		// translate to view coordinates
		x -= scrollX;
		y -= scrollY;
		// apply zoom factor
		x *= zoom;
		y *= zoom;
		// get location relative to center of view
		Rectangle ca = getClientArea();
		x += ca.x + ca.width / 2;
		y += ca.y + ca.height / 2;
		return new Point2d(x, y);
	}

	/**
	 * <p>
	 * Make sure a point lies within the image's coordinates.
	 * </p>
	 * <p>
	 * <code>clampImageCoordinates</code> checks whether a point lies within the currently loaded
	 * image, and if not, changes it to the nearest point that does.
	 * </p>
	 * 
	 * @param p
	 *            the point to check
	 */
	private void clampImageCoordinates(Point2d p) {
		ImageData imgData = image.getImageData();
		if (p.x < 0)
			p.x = 0;
		else if (p.x >= imgData.width - 1)
			p.x = imgData.width - 1;
		if (p.y < 0)
			p.y = 0;
		else if (p.y >= imgData.height - 1)
			p.y = imgData.height - 1;
	}

	@Override
	public void paintControl(PaintEvent e) {
		Rectangle ca = getClientArea();
		if (image != null) {
			Point2d topLeftImage = viewToWorld(new Point2d(ca.x, ca.y));
			Point2d bottomRightImage = viewToWorld(new Point2d(ca.x + ca.width, ca.y + ca.height));

			topLeftImage.floor();
			bottomRightImage.ceil();

			clampImageCoordinates(topLeftImage);
			clampImageCoordinates(bottomRightImage);

			Point2d topLeftView = worldToView(topLeftImage);
			Point2d bottomRightView = worldToView(bottomRightImage);

			// draw image
			e.gc.drawImage(image, (int) Math.round(topLeftImage.x),
					(int) Math.round(topLeftImage.y),
					(int) Math.round(bottomRightImage.x - topLeftImage.x),
					(int) Math.round(bottomRightImage.y - topLeftImage.y),
					(int) Math.round(topLeftView.x), (int) Math.round(topLeftView.y),
					(int) Math.round(bottomRightView.x - topLeftView.x),
					(int) Math.round(bottomRightView.y - topLeftView.y));

			Project p = getState().getProject();
			// draw feature connections
			for (Feature f : visibleImage.getFeatures()) {
				Point2d floc = worldToView(visibleImage.getLocationInView(f));
				Set<Feature> connectedFeatures = p.connectedFeatures(f);
				for (Feature f2 : connectedFeatures) {
					if (f2.getId() < f.getId()) {
						HasCoordinates2d f2l = visibleImage.getLocationInView(f2);
						if (f2l != null) {
							Point2d f2loc = worldToView(f2l);
							e.gc.drawLine((int)floc.x, (int)floc.y, (int)f2loc.x, (int)f2loc.y);
						}
					}
				}
			}

			// draw features
			for (Feature f : visibleImage.getFeatures()) {
				HasCoordinates2d location = visibleImage.getLocationInView(f);
				Point2d viewLocation = worldToView(location);
				e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_WHITE));
				e.gc.fillOval((int) Math.round(viewLocation.x - 4),
						(int) Math.round(viewLocation.y - 4), 7, 7);
				if (f.equals(getState().getSelectedFeature()))
					e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_GREEN));
				else
					e.gc.setBackground(Display.getDefault().getSystemColor(SWT.COLOR_BLUE));
				e.gc.fillOval((int) Math.round(viewLocation.x - 3),
						(int) Math.round(viewLocation.y - 3), 5, 5);
			}
			
		} else {
			Point p = e.gc.stringExtent(CHOOSE_IMAGE);
			e.gc.drawText(CHOOSE_IMAGE, ca.x + (ca.width - p.x) / 2, ca.y + ca.height / 2 - p.y);

			p = e.gc.stringExtent(DROP_IMAGE);
			e.gc.drawText(DROP_IMAGE, ca.x + (ca.width - p.x) / 2, ca.y + ca.height / 2);
		}
	}

	@Override
	public void mouseScrolled(MouseEvent e) {
		// FIXME zoom towards the mouse position instead of towards the center of the view
		zoom *= Math.pow(ZOOM_SPEED, -e.count);
		zoom = Math.min(MAX_ZOOM, Math.max(MIN_ZOOM, zoom));
		redraw();
	}

	/**
	 * Called when a feature has changed.
	 * 
	 * @param f
	 */
	public void featureChanged(Feature f) {
		redraw();
	}

}
